<script setup lang="ts">
import { ref } from 'vue'
import {
  VueFlow,
  useVueFlow,
  ConnectionLineOptions,
  MarkerType,
  GraphNode,
  GraphEdge,
  Connection,
  rendererPointToPoint
} from '@vue-flow/core'
import { Background } from '@vue-flow/background'
import { Controls } from '@vue-flow/controls'
import '@vue-flow/core/dist/style.css'
import '@vue-flow/core/dist/theme-default.css'
import '@vue-flow/controls/dist/style.css'
import ScriptService from '@/services/script.service'
import OutputService from '@/services/output.service'
import { NodeItem } from '@/types/node'
import { ArgumentItem } from '@/types/argument'
import FlowNode from './node.vue'
import ConnectionLine from './connection-line.vue'
import FlowNodeRoot from './node-root.vue'
import FlowNodeConst from './node-const.vue'
import FlowNodeDebug from './node-debug.vue'
import ContextMenu from './context-menu.vue'

type NodeData = Omit<NodeItem, 'inputs' | 'outputs'> & {
  inputs: (ArgumentItem & { active: boolean, value: Any })[],
  outputs: (ArgumentItem & { active: boolean, value: Any })[]
}
type NodeExpose = { run: (arg: Any) => Promise<Any>, focus: () => void }

const scriptService = new ScriptService()
const outputService = new OutputService()
const nodes = new Map<string, NodeExpose>()
const connectionLineOption = {
  type: 'default'
} as ConnectionLineOptions
const _vueFlow = useVueFlow({
  nodes: [{
    id: 'root',
    type: 'root',
    label: '图像选取',
    position: { x: 50, y: 360 },
    selectable: false,
    deletable: false,
    data: {
      id: 'root',
      inputs: [],
      outputs: [{
        name: 'url',
        type: 'string'
      }]
    }
  }],
  edges: []
})
const contextFilter = ref('')
const contextPosition = ref({
  x: 0,
  y: 0
})
const penddingConnection = ref<Connection & { io: string, type: string, x: number, y: number }>()

_vueFlow.onConnect(param => {
  onEdgeUpdate({ edge: undefined, connection: param })
})
function setNodeRefs (el: Any, id: string) {
  nodes.set(id, el)
}
function onEdgeAdded (edge: GraphEdge, update = true) {
  const source: GraphNode<NodeData> | undefined = _vueFlow.nodes.value.find(v1 => v1.id === edge.source)
  if (source) {
    const output = source.data.outputs.find(v1 => v1.name === edge.sourceHandle)
    if (output) {
      output.active = true
    }
  }
  const target: GraphNode<NodeData> | undefined = _vueFlow.nodes.value.find(v1 => v1.id === edge.target)
  if (target) {
    const input = target.data.inputs.find(v1 => v1.name === edge.targetHandle)
    if (input) {
      input.active = true
    }
  }
  if (target?.type === 'debug') {
    const node = nodes.get(target.id)
    node?.run({ i: { type: source!.data.outputs[0].type, value: source!.data.outputs[0].value } })
    // 约定类型
    target.data.inputs[0].type = source!.data.outputs[0].type
    target.data.outputs[0].type = source!.data.outputs[0].type
  }
  if (update) {
    onUpdate()
  }
}
function onEdgeRemoved (edge: GraphEdge) {
  let node: GraphNode<NodeData> | undefined = _vueFlow.nodes.value.find(v1 => v1.id === edge.source)
  if (node) {
    const output = node.data.outputs.find(v1 => v1.name === edge.sourceHandle)
    if (output) {
      output.active = false
    }
    setTimeout(() => {
      if (node?.type === 'debug' && _vueFlow.edges.value.findIndex(v => v.source === node?.id || v.target === node?.id) < 0) {
        // 重置类型
        node.data.inputs[0].type = 'any'
        node.data.outputs[0].type = 'any'
        console.log(node)
      }
    }, 250)
  }
  node = _vueFlow.nodes.value.find(v1 => v1.id === edge.target)
  if (node) {
    const input = node.data.inputs.find(v1 => v1.name === edge.targetHandle)
    if (input) {
      input.active = false
    }
    setTimeout(() => {
      if (node.type === 'debug' && _vueFlow.edges.value.findIndex(v => v.source === node?.id || v.target === node?.id) < 0) {
      // 重置类型
        node.data.inputs[0].type = 'any'
        node.data.outputs[0].type = 'any'
      }
    }, 250)
  }
  onUpdate()
}
function genID () {
  let id = 'abcdefghijklmnopqrstuvwxyz'[Math.round(Math.random() * 25)]
  for (let i = 0; i < 7; i++) {
    id += 'abcdefghijklmnopqrstuvwxyz0123456789'[Math.round(Math.random() * 35)]
  }
  return id
}
function onNodeSelect (node: NodeItem, position: { x: number, y: number }) {
  let id = genID()
  while (_vueFlow.findNode(id)) {
    id = genID()
  }
  // 拖动连接增加的节点
  let connection: Any | undefined
  if (penddingConnection.value) {
    let arg: ArgumentItem | undefined
    connection = { ...penddingConnection.value, type: undefined } as Any
    const _type = penddingConnection.value!.type
    penddingConnection.value = undefined
    if (connection.io === 'input') {
      connection.target = id
      arg = node.inputs.find(v => v.type === 'any' || v.type === _type)
      connection.targetHandle = arg?.name
    } else {
      // 如果上游是 const 需要自动变类型
      if (node.type === 'const') {
        node.outputs[0].type = _type;
        (node.outputs[0] as Any).value = { number: 0, string: '', bool: false }[_type]
      }
      connection.source = id
      arg = node.outputs.find(v => v.type === 'any' || v.type === _type)
      connection.sourceHandle = arg?.name
    }
    if (!arg) {
      connection = undefined
    } else {
      position.x = connection.x
      position.y = connection.y
      if (connection.io === 'output') {
        position.x -= ({
          debug: 120,
          const: 240
        }[node.type!] || 120)
      }
    }
  }
  _vueFlow.addNodes([{
    id,
    type: node.type || 'script',
    label: node.name,
    position: _vueFlow.project(position),
    data: {
      ...node,
      inputs: JSON.parse(JSON.stringify(node.inputs || [])),
      outputs: JSON.parse(JSON.stringify(node.outputs || []))
    }
  } as Any])
  if (connection) {
    onEdgeUpdate({ edge: undefined, connection })
  } else {
    onUpdate()
  }
}
function onDrop (evt: DragEvent) {
  const data = evt.dataTransfer?.getData('Text') as string
  const { left, top } = _vueFlow.vueFlowRef.value!.getBoundingClientRect()
  try {
    const node = JSON.parse(data) as NodeItem
    onNodeSelect(node, { x: evt.clientX - left, y: evt.clientY - top })
  } catch {

  }
}
function onNodeRemove (id: string) {
  _vueFlow.removeNodes(id)
  const edges = _vueFlow.edges.value.filter(v => v.source === id || v.target === id)
  _vueFlow.removeEdges(edges)
  nodes.delete(id)
}
function onEdgeDbClick ({ edge }: { edge: GraphEdge }) {
  onEdgeRemoved(edge)
  _vueFlow.removeEdges([edge])
}
function onEdgeUpdate ({ edge, connection }: { edge: GraphEdge | undefined, connection: Connection }) {
  // 判断两端类型是否一致且输入输出方向正确
  const source: GraphNode<NodeData> | undefined = _vueFlow.nodes.value.find(v => v.id === connection.source)
  const output = source?.data.outputs.find(v => v.name === connection.sourceHandle)
  const target: GraphNode<NodeData> | undefined = _vueFlow.nodes.value.find(v => v.id === connection.target)
  const input = target?.data.inputs.find(v => v.name === connection.targetHandle)
  if (!input || !output) {
    return
  }
  if (output.type !== input.type && input.type !== 'any' && output.type !== 'any') {
    return
  }
  if (!edge) {
    // 删除已经连的线
    const edges = _vueFlow.edges.value.filter(v => v.target === connection.target && v.targetHandle === connection.targetHandle)
    _vueFlow.removeEdges(edges)
    // 将相连的输出端置为未连接状态
    edges.forEach(v => {
      onEdgeRemoved(v)
    })
    // 将新连接的两端置为连接状态
    _vueFlow.addEdges([{
      ...connection,
      animated: true,
      // selectable: false,
      class: 'vscript-edge',
      id: genID(),
      markerEnd: MarkerType.Arrow
    }])
    onEdgeAdded(connection as GraphEdge)
  } else {
    onEdgeRemoved(edge)
    _vueFlow.updateEdge(edge, connection)
    onEdgeAdded(connection as GraphEdge)
  }
}
async function onUpdate () {
  if (_vueFlow.nodes.value.length < 2 || _vueFlow.edges.value.length < 1) {
    return
  }
  await new Promise(res => setTimeout(res, 150))
  try {
    const code = await scriptService.compile(_vueFlow.nodes.value, _vueFlow.edges.value)
    if (code) {
      outputService.script(code)
    }
  } catch (e: Any) {
    outputService.error(e.message || e.toString?.() || e)
  }
}
function onPaneContextMenu (evt: MouseEvent) {
  penddingConnection.value = undefined
  contextFilter.value = ''
  contextPosition.value = {
    x: evt.offsetX,
    y: evt.offsetY
  }
}
function hideContextmenu () {
  contextPosition.value = {
    x: -1000,
    y: 0
  }
}
function onConnectCancel (nid: string, hid: string, x: number, y: number) {
  penddingConnection.value = undefined
  const node: GraphNode<NodeData> | undefined = _vueFlow.findNode(nid)
  const filters = [] as string[]
  let arg = node?.data.outputs.find(v => v.name === hid)
  if (arg) {
    // 输出 any 说明还未连接确认类型
    if (arg.type === 'any') {
      return
    }
    filters.push('input:' + arg.type)
  } else {
    arg = node?.data.inputs.find(v => v.name === hid)
    if (!arg) {
      return
    }
    // 输出 any 说明还未连接确认类型
    if (arg.type === 'any') {
      return
    }
    filters.push('output:' + arg.type)
  }
  const pos = rendererPointToPoint({ x, y }, _vueFlow.viewport.value)
  contextFilter.value = filters.join(',')
  setTimeout(() => {
    contextPosition.value = {
      x: pos.x,
      y: pos.y
    }
  }, 50)
  const io = filters[0].split(':')[0]
  if (io === 'input') {
    penddingConnection.value = {
      source: nid,
      sourceHandle: hid,
      io: filters[0].split(':')[0],
      type: arg.type,
      x: pos.x,
      y: pos.y
    } as Any
  } else {
    penddingConnection.value = {
      target: nid,
      targetHandle: hid,
      io: filters[0].split(':')[0],
      type: arg.type,
      x: pos.x,
      y: pos.y
    } as Any
  }
}

setInterval(() => {
  _vueFlow.getSelectedNodes.value.forEach(v => {
    nodes.get(v.id)?.focus?.()
  })
}, 200)
window.addEventListener('mousedown', () => {
  _vueFlow.getSelectedNodes.value.forEach(v => {
    v.selected = false
  })
  hideContextmenu()
})
defineExpose({
  async run () {
    if (_vueFlow.nodes.value.length < 2 || _vueFlow.edges.value.length < 1) {
      return
    }
    const code = await scriptService.compile(_vueFlow.nodes.value, _vueFlow.edges.value)
    if (code) {
      outputService.script(code)
      const res = await scriptService.run(code)
      for (const node of _vueFlow.nodes.value.filter(v => v.type === 'debug')) {
        const edge = _vueFlow.edges.value.find(v => v.target === node.id)
        const source: GraphNode<NodeData> | undefined = _vueFlow.nodes.value.find(v => v.id === edge?.source)
        if (!source) {
          continue
        }
        const inst = nodes.get(node.id)
        inst?.run({
          i: {
            type: source.data.outputs.find(v => v.name === edge?.sourceHandle)?.type || 'any',
            value: res[`r_${node.id}`]
          }
        })
      }
      outputService.debug(`${new Date().toLocaleTimeString()} 执行成功: ${JSON.stringify(res)}`)
    }
  },
  json () {
    const nodes = _vueFlow.nodes.value.map(v => ({
      id: v.id,
      label: v.label,
      type: v.type,
      position: v.position,
      data: v.data,
      selectable: v.selectable,
      deletable: v.deletable
    }))
    const edges = _vueFlow.edges.value.map(v => ({
      id: v.id,
      source: v.source,
      target: v.target,
      sourceHandle: v.sourceHandle,
      targetHandle: v.targetHandle
    }))
    return {
      nodes,
      edges
    }
  },
  async reset (_nodes: Any[], _edges: Any[]) {
    _vueFlow.removeEdges(_vueFlow.edges.value)
    _vueFlow.removeNodes(_vueFlow.nodes.value)
    _vueFlow.addNodes(_nodes)
    _edges.forEach(v => v.animated = true)
    _vueFlow.addEdges(_edges)
    await new Promise(res => setTimeout(res, 150))
    _edges.forEach(async v => {
      while (!nodes.has(v.target)) {
        await new Promise(res => setTimeout(res, 10))
      }
      onEdgeAdded(v, false)
    })
    onUpdate()
  }
})
</script>

<template>
  <div class="vscript" @dragover.prevent @click.stop>
    <vue-flow snap-to-grid :snap-grid="[5, 5]" :select-nodes-on-drag="false" edges-updatable
      :connection-line-options="connectionLineOption" default-marker-color="#DDD" @drop="evt => onDrop(evt as Any)"
      @dragover.prevent @edge-update="onEdgeUpdate" @edge-double-click="onEdgeDbClick"
      @pane-context-menu="onPaneContextMenu" @viewport-change-start="hideContextmenu" @click="hideContextmenu"
      @edge-update-start="hideContextmenu" @node-drag-start="hideContextmenu">
      <template #connection-line="data">
        <ConnectionLine v-bind="data" @connect-end="onConnectCancel" />
      </template>
      <template #node-root="data">
        <flow-node-root :ref="el => setNodeRefs(el, data.id)" :id="data.id" :label="data.label"
          :outputs="data.data.outputs" @update:value="v => data.data.outputs[0].value = v" />
      </template>
      <template #node-const="data">
        <flow-node-const :ref="el => setNodeRefs(el, data.id)" :id="data.id" :label="data.label"
          :selected="data.selected" :outputs="data.data.outputs" @update:type="v => data.data.outputs[0].type = v"
          @update:value="v => data.data.outputs[0].value = v" @remove="() => onNodeRemove(data.id)" />
      </template>
      <template #node-debug="data">
        <flow-node-debug :ref="el => setNodeRefs(el, data.id)" :id="data.id" :label="data.label"
          :selected="data.selected" :inputs="data.data.inputs" :outputs="data.data.outputs"
          @remove="() => onNodeRemove(data.id)" />
      </template>
      <template #node-script="data">
        <flow-node :ref="el => setNodeRefs(el, data.id)" :id="data.id" :label="data.label" :selected="data.selected"
          :inputs="data.data.inputs" :outputs="data.data.outputs" :script="data.data.script"
          @remove="() => onNodeRemove(data.id)" />
      </template>
      <Background :gap="15" />
      <Controls />
    </vue-flow>
    <ContextMenu :filter="contextFilter" :position="contextPosition" @select="onNodeSelect" />
  </div>
</template>
<style scoped lang="less">
.vscript {
  position: relative;
  height: 100%;
  overflow: hidden;

  .vue-flow {
    width: 100%;
    height: 100%;

    :deep(.vscript-edge.selected .vue-flow__edge-path) {
      stroke: rgb(72, 218, 255) !important;
    }
  }
}
</style>
